package ch.ethz.fcl.metrobuzz.render.techniques.projection;

import java.nio.FloatBuffer;
import java.nio.IntBuffer;

import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.util.glu.GLU;

import ch.ethz.fcl.metrobuzz.spatialindex.object2d.Point2D;
import ch.ethz.fcl.metrobuzz.spatialindex.object2d.Rectangle;
import ch.ethz.fcl.metrobuzz.spatialindex.object3d.Point3D;

public class Projection {
	// The model view for caching
	private static FloatBuffer modelview = BufferUtils.createFloatBuffer(16);
	// The projection for caching
	private static FloatBuffer projection = BufferUtils.createFloatBuffer(16);
	// The view port for caching
	private static IntBuffer viewport = BufferUtils.createIntBuffer(16);
	// A temporary buffer
	private static FloatBuffer tempNearBuffer = FloatBuffer.allocate(3);
	// A temporary buffer
	private static FloatBuffer tempFarBuffer = FloatBuffer.allocate(3);

	private void updateArrays() {
		modelview.clear();
		projection.clear();
		viewport.clear();
		GL11.glGetFloat(GL11.GL_MODELVIEW_MATRIX, modelview);
		GL11.glGetFloat(GL11.GL_PROJECTION_MATRIX, projection);
		GL11.glGetInteger(GL11.GL_VIEWPORT, viewport);
	}

	public Rectangle getBound(float minX, float minY, float maxX, float maxY,
			float z) {
		Point2D windowMin = new Point2D(minX, minY);
		Point2D windowMax = new Point2D(maxX, maxY);

		Point3D worldTopLeft = getWorldCoord(windowMin, z);
		Point3D worldBtmRight = getWorldCoord(windowMax, z);

		return new Rectangle(worldTopLeft.getX(), worldTopLeft.getY(),
				worldBtmRight.getX(), worldBtmRight.getY());
	}

	// Get the position inside the application from a passed position on the
	// applet together with depth, realPos gets set to the return value
	public Point3D getWorldCoord(Point2D p, float zPos) {
		if (p == null) {
			return null;
		}

		updateArrays();
		// Get the coordinates at the near plane
		tempNearBuffer.clear();
		GLU.gluUnProject(p.getX(), p.getY(), 0, modelview, projection,
				viewport, tempNearBuffer);
		// Get the coordinates at the far plane
		tempFarBuffer.clear();
		GLU.gluUnProject(p.getX(), p.getY(), 1, modelview, projection,
				viewport, tempFarBuffer);

		if (tempNearBuffer.get(2) == tempFarBuffer.get(2))
			return null;

		float t = (tempNearBuffer.get(2) - zPos)
				/ (tempNearBuffer.get(2) - tempFarBuffer.get(2));

		float x = tempNearBuffer.get(0)
				+ (tempFarBuffer.get(0) - tempNearBuffer.get(0)) * t;
		float y = tempNearBuffer.get(1)
				+ (tempFarBuffer.get(1) - tempNearBuffer.get(1)) * t;

		return new Point3D(x, y, zPos);
	}

	public Point2D getScreenCoord(Point3D p) {
		if (p == null) {
			return null;
		}

		updateArrays();
		tempFarBuffer.clear();
		GLU.gluProject(p.getX(), p.getY(), p.getZ(), modelview, projection,
				viewport, tempFarBuffer);

		float x = tempFarBuffer.get(0);
		float y = tempFarBuffer.get(1);

		return new Point2D(x, y);
	}

	/**
	 * return two point p1 & p2 distance on the screen at plane z = 0
	 * 
	 * @param p1
	 * @param p2
	 * @return
	 */
	public float getScreenDistance(Point2D p1, Point2D p2) {
		Point2D p1screen = getScreenCoord(new Point3D(p1.getX(), p1.getY(), 0));
		Point2D p2screen = getScreenCoord(new Point3D(p2.getX(), p2.getY(), 0));

		return p1screen.distance(p2screen);
	}

	public float getScreenDistance(float dist) {
		Point2D p1screen = getScreenCoord(new Point3D(0, 0, 0));
		Point2D p2screen = getScreenCoord(new Point3D(dist, 0, 0));

		return p1screen.distance(p2screen);
	}

	public float getWorldDistance(float screenDist) {
		Point3D p1world = getWorldCoord(new Point2D(0, 0), 0);
		Point3D p2world = getWorldCoord(new Point2D(0, screenDist), 0);
		return p1world.distance(p2world);
	}
}
